﻿/// <summary>
/// Developer: CHOOI KAH WAI
/// Last Modified: 16/11/2009
/// </summary>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.ObjectModel;
using System.Net.Sockets;
using System.Net;
using System.Windows.Threading;
using System.Windows;
using System.IO;
using System.ComponentModel;
using System.Xml;
using ARSS.Host.Visual;
using System.Xml.Linq;
using ARSS.Common;
using ARSS.Host.Common;
using System.Timers;

namespace ARSS.Host
{
    /// <summary>
    /// System Core
    /// Controls entire system logic and flow
    /// </summary>
    class Core
    {
        #region Variables

        /// <summary>
        /// Core Singleton
        /// </summary>
        private static Core thisInstance = null;

        /// <summary>
        /// Hosting socket
        /// </summary>
        private Socket serverSocket;

        /// <summary>
        /// Unauthorized network clients
        /// </summary>
        private ArrayList pendingClients;

        /// <summary>
        /// Holds SessionWindow instance
        /// </summary>
        private SessionWindow sessionWindowObject; 
   
        #endregion

        #region Properties

        /// <summary>
        /// Is session started?
        /// </summary>
        public bool IsSessionStarted { get; set; }

        /// <summary>
        /// Client list
        /// </summary>
        public ObservableCollection<ClientData> ClientList { get; set; }

        /// <summary>
        /// Current active window
        /// </summary>
        public Window ActiveWindowObject { get; set; }

        /// <summary>
        /// File path to the scene data
        /// </summary>
        public string SceneFilePath { get; set; }

        /// <summary>
        /// List of scene animations
        /// </summary>
        public ObservableCollection<string> SceneAnimationList { get; set; }

        /// <summary>
        /// Get Core Singleton
        /// </summary>
        public static Core Instance
        {
            get
            {
                if (thisInstance == null)
                    thisInstance = new Core();
                return thisInstance;
            }
        }

        /// <summary>
        /// Root scene node
        /// </summary>
        public SceneNode RootSceneNode { get; set; }

        /// <summary>
        /// Is host recording scene?
        /// </summary>
        public bool IsRecording { get; set; }

        /// <summary>
        /// Temporary scene data holder
        /// </summary>
        public ObservableCollection<KeyFrame> TempSceneData { get; set; }

        /// <summary>
        /// Scene recording starting time
        /// </summary>
        public DateTime RecordingTime { get; set; }

        #endregion


        #region Functions

        /// <summary>
        /// Constructor
        /// </summary>
        private Core()
        {
            Logger.Instance.DirectoryName = "HostLog";
            Logger.Instance.IsActivated = true;
            
            ClientList = new ObservableCollection<ClientData>();
            pendingClients = new ArrayList();
            IsSessionStarted = false;

            IsRecording = false;
        }

        /// <summary>
        /// Start hosting
        /// </summary>
        /// <param name="port">Listening port</param>
        public void StartHosting(int port)
        {
            this.serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);
            this.serverSocket.Bind(localEndPoint);

            // Accept up to 30 clients queue
            this.serverSocket.Listen(30);

            // Client connected event
            SocketAsyncEventArgs evt = new SocketAsyncEventArgs();
            evt.Completed +=new EventHandler<SocketAsyncEventArgs>(OnClientConnected);
            this.serverSocket.AcceptAsync(evt);

        }

        /// <summary>
        /// Client connected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void OnClientConnected(object sender, SocketAsyncEventArgs e)
        {
            if (e.AcceptSocket.RemoteEndPoint != null)
            {
                Logger.Instance.Log("Client IP: " + e.AcceptSocket.RemoteEndPoint.ToString() + " connected.");
            }
            else
            {
                Logger.Instance.Log("Client connected.");
            }
            // Do not accept client when the session started
            if (Core.Instance.IsSessionStarted)
            {
                try
                {
                    e.AcceptSocket.Disconnect(true);
                }
                catch (Exception ex)
                {
                    Logger.Instance.Log("Could not disconnect client, reason: " + ex.Message);
                }
                return;
            }

            if (e.SocketError == SocketError.Success)
            {
                ActiveWindowObject.Dispatcher.Invoke((Action)delegate
                {
                    try
                    {
                        // Create new client instance
                        ClientData connectedClient = new ClientData();
                        connectedClient.TCPSocket = e.AcceptSocket;
                        this.pendingClients.Add(connectedClient);

                        connectedClient.ReceiveData();

                        connectedClient.DataReceived += new ClientData.DataReceivedHandler(OnDataReceived);
                        connectedClient.ClientDisconnected += new ClientData.ClientDisconnectedHandler(OnClientDisconnected);
                    }
                    catch (Exception ex)
                    {
                        Logger.Instance.Log("Could not accept client, reason: " + ex.Message);
                    }

                }, null);

                // Don't stop accepting client
                SocketAsyncEventArgs evt = new SocketAsyncEventArgs();
                evt.Completed += new EventHandler<SocketAsyncEventArgs>(OnClientConnected);
                this.serverSocket.AcceptAsync(evt);
            }
        }

        /// <summary>
        /// Stop hosting
        /// </summary>
        public void StopHosting()
        {
            // Disconnect all pending clients
            foreach (ClientData client in this.pendingClients)
            {
                client.TCPSocket.Shutdown(SocketShutdown.Both);
                client.TCPSocket.Close();
            }
            this.pendingClients.Clear();

            // Disconnect all authorized clients
            foreach (ClientData client in ClientList)
            {
                client.TCPSocket.Shutdown(SocketShutdown.Both);
                client.TCPSocket.Close();
            }
            ClientList.Clear();

            // Start closing the socket here
            try
            {
                this.serverSocket.Close();
            }
            catch (Exception ex)
            {
                Logger.Instance.Log("Could not close server socket, reason: " + ex.Message);
            }

            if (this.IsSessionStarted)
                StopSession();

            
        }


        /// <summary>
        /// Client disconnected
        /// </summary>
        /// <param name="sender"></param>
        protected void OnClientDisconnected(ClientData sender)
        {
            Logger.Instance.Log("Client Nickname: " + sender.Name + " disconnected.");
            ActiveWindowObject.Dispatcher.Invoke((Action)delegate
            {
                // Remove client from list

                if (this.pendingClients.Contains(sender))
                    this.pendingClients.Remove(sender);
                else
                    ClientList.Remove(sender);
            }, null);
        }

        /// <summary>
        /// Client data received
        /// Process the data here
        /// </summary>
        /// <param name="sender">Data originator</param>
        /// <param name="data">Actual data</param>
        protected void OnDataReceived(ClientData sender, byte[] data)
        {
            MemoryStream buffer = new MemoryStream(data);

            byte[] intBuffer = new byte[4];
            buffer.Read(intBuffer, 0, 4);

            int actionCode = BitConverter.ToInt32(intBuffer, 0);
            switch (actionCode)
            {
                case (int)NetworkActionCodesEnum.UserNickname:
                    {
                        // Get nickname length
                        buffer.Read(intBuffer, 0, 4);
                        int length = BitConverter.ToInt32(intBuffer, 0);
                        byte[] nameBuffer = new byte[length];
                        // Get nickname data
                        buffer.Read(nameBuffer, 0, length);
                        sender.Name = System.Text.ASCIIEncoding.UTF8.GetString(nameBuffer);

                        ActiveWindowObject.Dispatcher.Invoke((Action)delegate
                        {
                            ClientList.Add(sender);
                            SendSceneData(sender);
                        }, null);
                        this.pendingClients.Remove(sender);
                    }
                    break;

                case (int)NetworkActionCodesEnum.LockNode:
                    {
                        // Get node name length
                        buffer.Read(intBuffer, 0, 4);
                        int length = BitConverter.ToInt32(intBuffer, 0);
                        byte[] nodeName = new byte[length];
                        // Get node name data
                        buffer.Read(nodeName, 0, length);
                        // Get locker nickname length
                        buffer.Read(intBuffer, 0, 4);
                        length = BitConverter.ToInt32(intBuffer, 0);
                        byte[] nickName = new byte[length];
                        // Get locker nickname data
                        buffer.Read(nickName, 0, length);

                        // Append locker nickname to the node name
                        SceneNode lockedNode = FindSceneNode(System.Text.Encoding.UTF8.GetString(nodeName), RootSceneNode);
                        lockedNode.Locker = System.Text.Encoding.UTF8.GetString(nickName);
                        lockedNode.Name += " < " + lockedNode.Locker + " > ";

                        // Notify other clients
                        BroadcastData(data, data.Length, sender);
                    }
                    break;
                case (int)NetworkActionCodesEnum.ReleaseNode:
                    {
                        // Get node name length
                        buffer.Read(intBuffer, 0, 4);
                        int length = BitConverter.ToInt32(intBuffer, 0);
                        byte[] nodeName = new byte[length];
                        // Get node name data
                        buffer.Read(nodeName, 0, length);

                        // Append locker nickname to the node name
                        SceneNode lockedNode = FindSceneNode(System.Text.Encoding.UTF8.GetString(nodeName), RootSceneNode);
                        lockedNode.Name = lockedNode.Name.Substring(0, lockedNode.Name.IndexOf(" <"));
                        if (lockedNode.Locker.Length > 0)
                        {
                            lockedNode.Locker = "";

                            // Notify other clients
                            BroadcastData(data, data.Length, sender);
                        }
                    }
                    break;
                case (int)NetworkActionCodesEnum.UpdateMatrix:
                    // Forward this network packet to other clients
                    BroadcastData(data, data.Length, sender);
                    if (IsRecording)
                    {
                        buffer.Read(intBuffer, 0, 4);
                        int length = BitConverter.ToInt32(intBuffer, 0);
                        byte[] nodeName = new byte[length];
                        // Get node name data
                        buffer.Read(nodeName, 0, length);

                        // Read transformation matrix
                        double[] mat = new double[16];
                        byte[] singleMatrixBuffer = new byte[8];
                        for (int i = 0; i < 16; i++)
                        {
                            buffer.Read(singleMatrixBuffer, 0, 8);
                            mat[i] = BitConverter.ToDouble(singleMatrixBuffer, 0);
                        }

                        KeyFrame kf = new KeyFrame();
                        if (TempSceneData.Count == 0)
                        {
                            RecordingTime = DateTime.Now;
                            kf.TSec = 0;
                        }
                        else
                        {
                            kf.TSec = double.Parse(DateTime.Now.Subtract(RecordingTime).Seconds.ToString());
                        }

                        AnimationNode an = new AnimationNode();
                        an.Target = System.Text.Encoding.UTF8.GetString(nodeName);
                        an.X = mat[12];
                        an.Y = mat[13];
                        an.Z = mat[14];
                        kf.Nodes.Add(an);

                        TempSceneData.Add(kf);
                    }
                    break;
            }
            
        }

        /// <summary>
        /// Find SceneNode
        /// </summary>
        /// <param name="name">Scene node name</param>
        /// <param name="sourceNode">Node to identify</param>
        /// <returns>SceneNode</returns>
        protected SceneNode FindSceneNode(string name, SceneNode sourceNode)
        {
            SceneNode node = null;
            string nodeName = sourceNode.Name;

            // It might be locked by somebody, remove the locker name
            if (nodeName.Contains(" <"))
            {
                nodeName = nodeName.Remove(nodeName.IndexOf(" <"));
            }

            if (nodeName.CompareTo(name) != 0)
            {
                foreach (SceneNode child in sourceNode.Children)
                {
                    node = FindSceneNode(name, child);
                    if (node != null) break;
                }
            }
            else
            {
                node = sourceNode;
            }

            return node;
        }


        /// <summary>
        /// Send scene data to client
        /// </summary>
        /// <param name="receiver">Receiver client</param>
        public void SendSceneData(ClientData receiver)
        {
            FileStream file = new FileStream(SceneFilePath, FileMode.Open);

            byte[] buffer = new byte[file.Length];
            file.Read(buffer, 0, buffer.Length);
            file.Close();

            // Send nickname to server first
            MemoryStream ms = new MemoryStream();
            // Action Code
            ms.Write(BitConverter.GetBytes((int)NetworkActionCodesEnum.SendSceneData), 0, 4);
            // File size
            ms.Write(BitConverter.GetBytes(buffer.Length), 0, 4);
            // File buffer
            ms.Write(buffer, 0, buffer.Length);

            receiver.SendData(ms.GetBuffer(), 8 + buffer.Length);
        }

        /// <summary>
        /// Broadcast data to all clients
        /// </summary>
        /// <param name="data"></param>
        /// <param name="length"></param>
        public void BroadcastData(byte[] data, int length)
        {
            foreach (ClientData client in ClientList)
            {
                client.SendData(data, length);
            }
        }

        /// <summary>
        /// Overloaded broadcast function
        /// able to ignore particular client
        /// </summary>
        /// <param name="data">Data to braodcast</param>
        /// <param name="length">Data length</param>
        /// <param name="ignoreMe">Ignore this client</param>
        public void BroadcastData(byte[] data, int length, ClientData ignoreMe)
        {
            foreach (ClientData client in ClientList)
            {
                // skip if this is the client to ignore
                if (client == ignoreMe) continue;

                client.SendData(data, length);
            }
        }

        /// <summary>
        /// Load the scene node data from xml
        /// </summary>
        /// <param name="nodeElement">Xml element containing the node data</param>
        /// <returns>SceneNode</returns>
        protected SceneNode LoadNodeElement(XElement nodeElement)
        {
            SceneNode node = new SceneNode();

            node.Name = nodeElement.Attribute("name").Value;

            // Load children
            XElement childrenElement = nodeElement.Element("Children");
            if(childrenElement != null)
            {
                foreach (XElement child in childrenElement.Elements())
                {
                    SceneNode childNode = LoadNodeElement(child);
                    node.Children.Add(childNode);
                }
            }
            

            return node;
        }

        /// <summary>
        /// Load the scene file
        /// </summary>
        public void LoadSceneData()
        {
            XElement sceneElement = XElement.Load(SceneFilePath);

            // Load scene nodes
            RootSceneNode = LoadNodeElement(sceneElement.Element("Root"));

            // Load animations
            SceneAnimationList = new ObservableCollection<string>();

            var animationSet = from anim in sceneElement.Element("Animations").Elements("Set")
                             select new
                             {
                                 animationName = anim.Attribute("name").Value
                             };

            foreach (var animation in animationSet)
            {
                SceneAnimationList.Add(animation.animationName);
            }
        }

        /// <summary>
        /// Start Session
        /// </summary>
        public void StartSession()
        {
            try
            {
                LoadSceneData();
            }
            catch (Exception ex)
            {
                Logger.Instance.Log("Could not LoadSceneData, reason: " + ex.Message);
            }

            // Tell all clients to start session
            MemoryStream ms = new MemoryStream();
            // Action code
            ms.Write(BitConverter.GetBytes((int)NetworkActionCodesEnum.StartSession), 0, 4);
            BroadcastData(ms.ToArray(), (int)ms.Length);

            // Show session window dialog
            sessionWindowObject = new SessionWindow();
            sessionWindowObject.SceneComboBox.ItemsSource = SceneAnimationList;
            sessionWindowObject.Show();

            IsSessionStarted = true;

        }

        /// <summary>
        /// Stop Session
        /// </summary>
        public void StopSession()
        {
            sessionWindowObject.Close();
            sessionWindowObject = null;

            SceneAnimationList.Clear();
            SceneAnimationList = null;

            this.IsSessionStarted = false;

            Logger.Instance.Log("Session stopped.");
        }

        /// <summary>
        /// Set current scene
        /// </summary>
        /// <param name="animationName"></param>
        public void SetScene(string animationName)
        {
            // Broadcast animation name to all clients
            MemoryStream ms = new MemoryStream();
            // Action code
            ms.Write(BitConverter.GetBytes((int)NetworkActionCodesEnum.SetSceneAnimation), 0, 4);
            // Animation name length
            byte[] animationBuffer = System.Text.ASCIIEncoding.UTF8.GetBytes(animationName);
            ms.Write(BitConverter.GetBytes(animationBuffer.Length), 0, 4);
            // Animation name data
            ms.Write(animationBuffer, 0, animationBuffer.Length);

            BroadcastData(ms.ToArray(), (int)ms.Length);
        }

        /// <summary>
        /// Force release scene node
        /// </summary>
        /// <param name="nodeName"></param>
        public void ReleaseSceneNode(string nodeName)
        {
            SceneNode node = FindSceneNode(nodeName, RootSceneNode);

            // Broadcast message
            MemoryStream ms = new MemoryStream();
            // Action Code
            ms.Write(BitConverter.GetBytes((int)NetworkActionCodesEnum.ReleaseNode), 0, 4);
            byte[] nodeNameBuffer = System.Text.ASCIIEncoding.UTF8.GetBytes(nodeName);
            // Node Name Length
            ms.Write(BitConverter.GetBytes(nodeNameBuffer.Length), 0, 4);
            // Node Name buffer
            ms.Write(nodeNameBuffer, 0, nodeNameBuffer.Length);

            BroadcastData(ms.ToArray(), (int)ms.Length);
        }

        #endregion
    }
}
